We will create a side panel extension that gives basic information along with a screenshot, of the website at the current active tab. ![[Pasted image 20250327172744.png]] ## Setup Create icon files set and manifest.json file. You should know how by now. Create manifest.json with these contents: ``` { "manifest_version": 3, "name": "", "version": "1.0", "description": ".", "author": "Weng Fei Fung", "icons": { "16": "icon16x16.png", "32": "icon32x32.png", "48": "icon48x48.png", "128": "icon128x128.png" }, "content_security_policy": { "extension_pages": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'self';" }, "permissions": [ "sidePanel", "scripting", "tabs", "activeTab" ], "action": { "default_icon": "icon.png" }, "background": { "service_worker": "background.js" }, "side_panel": { "default_path": "sidepanel.html" }, "host_permissions": [ "https://*/*", "http://*/*" ] } ``` ^Notice the "sidePanel" as a permission. Otherwise, side panel api would not be available for you to programmatically open the side panel. ^ We've added a `data:` as an img-src CSP because we'll be capturing a screenshot of the website at the current active tab and placing it into the side panel. ^ background.js will actively listen to messages and onclick from chrome extension icon. ^ side panel's html file is opened programmatically. ^ further details on the flow of background/side panel will be discussed. Create sidepanel.html: ``` Options

Side Panel

``` Create sidepanel.js that sidepanel.html loads in: ``` // About to finish loading, send a message to the service worker that it's finished loading chrome.runtime.sendMessage({action: "sidepanel-domcontentloaded"}); // Data for html is received from background.js because side panel page DOM is finished loading and unlocked chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { console.log("Message received in sidepanel:", message); if (message.action === "sidepanel-dynamic-data") { console.log("Message of dynamic data received in sidepanel:", message); const {payload} = message; const {screenshotBase64Url} = message; const reportEl = document.getElementById("report") reportEl.innerHTML = ""; reportEl.innerHTML += `Site Stats:
`; reportEl.innerHTML += (()=>{ let str = ""; for (const [key, value] of Object.entries(payload)) { str+=`${key}: ${value}
` } return str; })(); reportEl.innerHTML += `
Screenshot:
`; const imgEl = document.createElement("img"); imgEl.src = screenshotBase64Url; imgEl.style.width = "100%"; imgEl.style.height = "100%"; imgEl.style.objectFit = "contain"; reportEl.appendChild(imgEl); } }); ``` Create background.js... Which helps coordinate the user clicking the chrome extension icon and the stats loading in the sidepanel that subsequently finishes loading (page needs to finish loading before we start writing to it): ``` console.log("Background script loaded"); chrome.action.onClicked.addListener((tab) => { chrome.tabs.query({ currentWindow: true }, function (tabs) { chrome.sidePanel.open({ windowId: tab.windowId }); // Hackish solution works, but not proper solution per docs // Proper solution is to have sidepanel ping back when domcontentloaded, then send message with data to render from background.js // setTimeout(() => { // }, 3000); }); }); chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { if (message.action === "sidepanel-domcontentloaded") { console.log("Message about sidepanel domcontentloaded received in background:", message); chrome.tabs.query({active: true, currentWindow: true}, async function(tabs) { const activeTab= tabs[0]; const title = activeTab.title; const url = activeTab.url; const faviconUrl = activeTab.favIconUrl; const screenshot = await chrome.tabs.captureVisibleTab(activeTab.windowId, {format: "png"}); const screenshotBase64 = screenshot.toString('base64'); const screenshotBase64Url = screenshotBase64; /** * Count the number of dom elements on the page * @return * Array * * where Result is an object with the following properties: * Result { * documentId: number * frameId: number * result: * } */ const executedResults = await chrome.scripting.executeScript({ target: {tabId: activeTab.id}, func: () => { const count = document.body.querySelectorAll('*').length; // console.log("Sidepanel document:", document); // console.log("Count of dom elements:", count); return count; } }); console.log("Executed results:", executedResults); const payload = { title, url, faviconUrl, countDomElements: executedResults[0].result } chrome.runtime.sendMessage({action: "sidepanel-dynamic-data", payload, screenshotBase64Url}); }); } // message.action is "sidepanel-domcontentloaded" }); ``` --- ## Discussion **How the logic flows is as follows:** 1. When user clicks the chrome extension icon, the background.js listens for and responds to onClicked by opening the sidepanel with `chrome.sidePanel.open` at the current window. 2. When sidepanel.html near finishes loading, its sidepanel.js sends a message "sidepanel-domcontentloaded" just because that line is next to execute `chrome.runtime.sendMessage({action: "sidepanel-domcontentloaded"});` in sidepanel.js. 3. The background.js listens for and responds to "sidepanel-domcontentloaded" and then performs an active tab query to get information about the webpage at the active tab (title, url, favIcon, etc). Also, it takes a screenshot of the webpage in view (part of Chrome's Tabs API!) and saves as a screenshot base 64 url. Next, an executeScript is run against the active tab in order to query for all \* elements and count their length, in other words, get the number of elements that are on the webpage. All this gathered information is sent along the message "sidepanel-dynanic-data" to the entire chrome extension environment (Recall that Chrome's sendMessage can also send data along with the message). 4. The sidepanel.js listens for and responds to that message "sidepanel-dynamic-data", and then receiving the information gathered from background.js, it renders the side panel stats to the user. **Two important new lessons here is that:** - A javascript file that sidepanel.html uses (but not pointed to by manifest.json) still has access to the chrome messaging system as proven by sidepanel.js responding to the message "sidepanel-dynamic-data". Even though sidepanel.js is not pointed by manifest.json, because the "side_panel"."default_path" points to the html file, then the js file that the html file loads will have access to Chrome API's. - That `await chrome.scripting.executeScript` call can return a value (though the returned data is in a wrapper object so you have to use the dot notation to access the returned value - refer to comments in the executeScript code at background.js). **Mermaid diagram (fyi):** ``` sequenceDiagram participant User participant ChromeExtension as Chrome Extension (icon) participant BackgroundJS as background.js participant SidePanel as sidepanel.html + sidepanel.js participant ActiveTab as Active Tab User->>ChromeExtension: Clicks extension icon ChromeExtension->>BackgroundJS: onClicked event BackgroundJS->>SidePanel: chrome.sidePanel.open() SidePanel-->>BackgroundJS: Message "sidepanel-domcontentloaded" Note over BackgroundJS,ActiveTab: Background.js asks active tab to give information back to background.js BackgroundJS<<->>ActiveTab: Query active tab info (title, url, favIcon) BackgroundJS<<->>ActiveTab: chrome.tabs.captureVisibleTab() → Screenshot BackgroundJS<<->>ActiveTab: chrome.scripting.executeScript() → get number of elements Note over BackgroundJS,ActiveTab: Final steps delivering the information in a message BackgroundJS-->>SidePanel: Message "sidepanel-dynamic-data" with information payload SidePanel->>SidePanel: Render side panel stats using received data ``` ![[Pasted image 20250328182853.png]] --- ## Testing Update/install the chrome extension. Then at google.com (not a new tab that by default has google opened because page access would be blocked from the extension), click the chrome extension icon to open the side panel and see stats. ![[Pasted image 20250327172744.png]] You can also test this at any other websites besides google.com